﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Content;
using Prison.Defence.Content.Pipleline.DataTypes;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using PrisonDefence.Models.Modifiers;

namespace PrisonDefence.Models
{
    class World
    {
        private Dictionary<int, ModelObject> models = new Dictionary<int, ModelObject>();
        private List<ModelInstance> instances = new List<ModelInstance>();
        private List<Light> lights = new List<Light>();

        private QuadRender quadRender = new QuadRender();

        private RenderTarget2D colorRT; //color and specular intensity
        private RenderTarget2D normalRT; //normals + specular power
        private RenderTarget2D depthRT; //depth
        private RenderTarget2D lightRT; //lighting

        private Effect clearBufferEffect;
        private Effect directionalLightEffect;

        private Effect pointLightEffect;
        private Model sphereModel; //point ligt volume

        private Effect finalCombineEffect;

        private SpriteBatch spriteBatch;

        private Vector2 halfPixel;

        private float lightRadius = 46.0f;
        private float lightIntensity = 2.5f;

        public World()
        {

        }

        public void LoadContent(GraphicsDevice graphics, ContentManager content, SceneDefinition scene)
        {
            LoadScene(content, scene);
            LoadLight(graphics, content);
        }

        private void LoadLight(GraphicsDevice graphics, ContentManager content)
        {
            halfPixel = new Vector2()
            {
                X = 0.5f / (float)graphics.PresentationParameters.BackBufferWidth,
                Y = 0.5f / (float)graphics.PresentationParameters.BackBufferHeight
            };

            int backbufferWidth = graphics.PresentationParameters.BackBufferWidth;
            int backbufferHeight = graphics.PresentationParameters.BackBufferHeight;

            quadRender.LoadContent();

            //colorRT = new RenderTarget2D(graphics, backbufferWidth, backbufferHeight, false, SurfaceFormat.Color, DepthFormat.Depth24);
            colorRT = new RenderTarget2D(graphics, backbufferWidth, backbufferHeight, false, SurfaceFormat.Color, DepthFormat.Depth24Stencil8);
            normalRT = new RenderTarget2D(graphics, backbufferWidth, backbufferHeight, false, SurfaceFormat.Color, DepthFormat.None);
            depthRT = new RenderTarget2D(graphics, backbufferWidth, backbufferHeight, false, SurfaceFormat.Single, DepthFormat.None);
            lightRT = new RenderTarget2D(graphics, backbufferWidth, backbufferHeight, false, SurfaceFormat.Color, DepthFormat.None);
            //colorRT = new RenderTarget2D(GraphicsDevice, backBufferWidth, backBufferHeight, false, SurfaceFormat.Color, DepthFormat.Depth24Stencil8);
            //normalRT = new RenderTarget2D(GraphicsDevice, backBufferWidth, backBufferHeight, false, SurfaceFormat.Color, DepthFormat.None);
            //depthRT = new RenderTarget2D(GraphicsDevice, backBufferWidth, backBufferHeight, false, SurfaceFormat.Single, DepthFormat.None);
            //lightRT = new RenderTarget2D(GraphicsDevice, backBufferWidth, backBufferHeight, false, SurfaceFormat.Color, DepthFormat.None);

            clearBufferEffect = content.Load<Effect>("FXs/ClearGBuffer");
            directionalLightEffect = content.Load<Effect>("FXs/DirectionalLight");
            finalCombineEffect = content.Load<Effect>("FXs/CombineFinal");
            pointLightEffect = content.Load<Effect>("FXs/PointLight");
            sphereModel = content.Load<Model>(@"Models/Sphere");

            spriteBatch = new SpriteBatch(graphics);
        }

        private void LoadScene(ContentManager content, SceneDefinition scene)
        {
            foreach (int key in scene.Models.Keys)
            {
                Model model = content.Load<Model>(scene.Models[key].ModelAssetName);
                models.Add(key, new ModelObject()
                {
                    Model = model,
                    Rotation = scene.Models[key].Rotation,
                    Scale = scene.Models[key].Scale
                });
            }

            foreach (ModelInstanceDefinition instanceDef in scene.ModelInstances)
            {
                if (!models.ContainsKey(instanceDef.ModelDefReference))
                    continue;

                ModelInstance modelInstance = new ModelInstance()
                {
                    ModelId = instanceDef.ModelDefReference,
                    Positon = instanceDef.Position,
                    Rotation = instanceDef.Rotation,
                    Scale = instanceDef.Scale,

                    Modifiers = LoadModifiers(instanceDef.Modifiers)
                };


                instances.Add(modelInstance);
            }

            foreach (LightDefinition lightDef in scene.Lights)
            {
                Light light = new Light()
                {
                    Color = lightDef.Color,
                    Direction = lightDef.Direction,
                    Intensity = lightDef.Intensity,
                    Position = lightDef.Position,
                    Radius = lightDef.Radius,
                    Type = lightDef.Type,

                    Modifiers = LoadModifiers(lightDef.Modifiers)
                };

                lights.Add(light);
            }
        }

        private List<IModifier> LoadModifiers(List<ModifierDefinition> list)
        {
            if (list == null)
                return null;

            List<IModifier> modifiers = new List<IModifier>();
            foreach (ModifierDefinition modDef in list)
            {
                if (modDef is RotationModifierDefinition)
                    modifiers.Add(new Rotator()
                    {
                        Position = (modDef as RotationModifierDefinition).Position,
                        Rotation = (modDef as RotationModifierDefinition).Rotation
                    });
            }

            return modifiers;
        }

        public void Update(GameTime gameTime)
        {
            foreach (Light light in lights.Where(l => l.Modifiers != null && l.Modifiers.Count > 0))
            {
                foreach(IModifier modifier in light.Modifiers)
                    modifier.Update(light);
            }
        }

        public void Draw(GraphicsDevice graphics, GameTime gameTime, Vector3 cameraPosition, Matrix view, Matrix projection)
        {
            SetGBuffer(graphics);
            ClearGBuffer(graphics);
            DrawScene(graphics, gameTime, cameraPosition, view, projection);
            ResolveGBuffer(graphics);
            DrawLights(graphics, gameTime, cameraPosition, view, projection);

            //spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Opaque, SamplerState.PointClamp, null, null);
            //spriteBatch.Draw((Texture2D)colorRT, new Rectangle(0, 0, 128, 128), Color.White);
            //spriteBatch.Draw((Texture2D)depthRT, new Rectangle(128, 0, 128, 128), Color.White);
            //spriteBatch.Draw((Texture2D)normalRT, new Rectangle(256, 0, 128, 128), Color.White);
            //spriteBatch.Draw((Texture2D)lightRT, new Rectangle(256 + 128, 0, 128, 128), Color.White);
            //spriteBatch.End();
        }

        #region Lighting

        private void SetGBuffer(GraphicsDevice graphics)
        {
            graphics.SetRenderTargets(colorRT, normalRT, depthRT);
        }

        private void ResolveGBuffer(GraphicsDevice graphics)
        {
            graphics.SetRenderTargets(null);
        }

        private void ClearGBuffer(GraphicsDevice graphics)
        {
            clearBufferEffect.Techniques[0].Passes[0].Apply();
            graphics.BlendState = BlendState.Opaque;
            quadRender.Draw(graphics, Vector2.One * -1, Vector2.One);
        }

        private void DrawDirectionalLight(GraphicsDevice graphics, Vector3 lightPosition, Vector3 lightDirection, Color color, Vector3 cameraPosition, Matrix view, Matrix projection)
        {
            directionalLightEffect.Parameters["colorMap"].SetValue(colorRT);
            directionalLightEffect.Parameters["normalMap"].SetValue(normalRT);
            directionalLightEffect.Parameters["depthMap"].SetValue(depthRT);

            directionalLightEffect.Parameters["lightDirection"].SetValue(lightDirection);
            directionalLightEffect.Parameters["Color"].SetValue(color.ToVector3());

            directionalLightEffect.Parameters["cameraPosition"].SetValue(lightPosition);
            directionalLightEffect.Parameters["InvertViewProjection"].SetValue(Matrix.Invert(view * projection));

            directionalLightEffect.Parameters["halfPixel"].SetValue(halfPixel);

            directionalLightEffect.Techniques[0].Passes[0].Apply();
            quadRender.Draw(graphics, Vector2.One * -1, Vector2.One);
        }

        private void DrawPointLight(GraphicsDevice graphics, Vector3 lightPosition, Color color, float lightRadius, float lightIntensity, Vector3 cameraPosition, Matrix view, Matrix projection)
        {
            //set the G-Buffer parameters
            pointLightEffect.Parameters["colorMap"].SetValue(colorRT);
            pointLightEffect.Parameters["normalMap"].SetValue(normalRT);
            pointLightEffect.Parameters["depthMap"].SetValue(depthRT);

            //compute the light world matrix
            //scale according to light radius, and translate it to light position
            Matrix sphereWorldMatrix = Matrix.CreateScale(lightRadius) * Matrix.CreateTranslation(lightPosition);
            pointLightEffect.Parameters["World"].SetValue(sphereWorldMatrix);
            pointLightEffect.Parameters["View"].SetValue(view);
            pointLightEffect.Parameters["Projection"].SetValue(projection);
            //light position
            pointLightEffect.Parameters["lightPosition"].SetValue(lightPosition);

            //set the color, radius and Intensity
            pointLightEffect.Parameters["Color"].SetValue(color.ToVector3());
            pointLightEffect.Parameters["lightRadius"].SetValue(lightRadius);
            pointLightEffect.Parameters["lightIntensity"].SetValue(lightIntensity);

            //parameters for specular computations
            pointLightEffect.Parameters["cameraPosition"].SetValue(cameraPosition);
            pointLightEffect.Parameters["InvertViewProjection"].SetValue(Matrix.Invert(view * projection));
            //size of a halfpixel, for texture coordinates alignment
            pointLightEffect.Parameters["halfPixel"].SetValue(halfPixel);
            //calculate the distance between the camera and light center
            float cameraToCenter = Vector3.Distance(cameraPosition, lightPosition);
            //if we are inside the light volume, draw the sphere's inside face
            if (cameraToCenter < lightRadius)
               graphics.RasterizerState = RasterizerState.CullClockwise;
            else
                graphics.RasterizerState = RasterizerState.CullCounterClockwise;

            graphics.DepthStencilState = DepthStencilState.None;

            pointLightEffect.Techniques[0].Passes[0].Apply();
            foreach (ModelMesh mesh in sphereModel.Meshes)
            {
                foreach (ModelMeshPart meshPart in mesh.MeshParts)
                {
                    graphics.Indices = meshPart.IndexBuffer;
                    graphics.SetVertexBuffer(meshPart.VertexBuffer);

                    graphics.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, meshPart.NumVertices, meshPart.StartIndex, meshPart.PrimitiveCount);
                }
            }

            graphics.RasterizerState = RasterizerState.CullCounterClockwise;
            graphics.DepthStencilState = DepthStencilState.Default;
        }

        float angle = 0.0f;

        private void DrawLights(GraphicsDevice graphics, GameTime gameTime, Vector3 cameraPosition, Matrix view, Matrix projection)
        {
            graphics.SetRenderTarget(lightRT);
            graphics.Clear(Color.Transparent);
            graphics.BlendState = BlendState.AlphaBlend;
            graphics.DepthStencilState = DepthStencilState.None;

            //DrawDirectionalLight(-camera.Direction, Color.Gray);

            angle += 0.001f;

            //Vector3[] lights = scene.Lights;
            //int n = 0;
            //if (lights != null)
            //{
            //    n = lights.Length;
            //    for (int i = 0; i < n; i++)
            //    {
            //        DrawPointLight(lights[i], Color.White, lightRadius, lightIntensity);

            //        //Vector3 pos = new Vector3((float)Math.Sin(i * MathHelper.TwoPi / n + angle), 0.30f, (float)Math.Cos(i * MathHelper.TwoPi / n + angle));
            //        //DrawPointLight(pos * 40, colors[i % 10], 150, 2);
            //        //pos = new Vector3((float)Math.Cos((i + 5) * MathHelper.TwoPi / n - angle), 0.30f, (float)Math.Sin((i + 5) * MathHelper.TwoPi / n - angle));
            //        //DrawPointLight(pos * 20, colors[i % 10], 20, 1);
            //        //pos = new Vector3((float)Math.Cos(i * MathHelper.TwoPi / n + angle), 0.10f, (float)Math.Sin(i * MathHelper.TwoPi / n + angle));
            //        //DrawPointLight(pos * 75, colors[i % 10], 45, 2);
            //        //pos = new Vector3((float)Math.Cos(i * MathHelper.TwoPi / n + angle), -0.3f, (float)Math.Sin(i * MathHelper.TwoPi / n + angle));
            //        //DrawPointLight(pos * 20, colors[i % 10], 20, 2);
            //    }
            //}

            //DrawDirectionalLight(camera.Direction, Color.White);

            foreach (Light light in lights.Where(l => l.Type == LightType.Point))
            {
                DrawPointLight(graphics, light.Position, light.Color, light.Radius, light.Intensity, cameraPosition, view, projection);
            }

            foreach (Light light in lights.Where(l => l.Type == LightType.Directional))
            {
                DrawDirectionalLight(graphics, light.Position, light.Direction, light.Color, cameraPosition, view, projection);
            }

            //DrawPointLight(graphics, new Vector3(0, (float)Math.Sin(angle * 0.8) * 40, 0), Color.Red, 30, 5, cameraPosition, view, projection);
            //DrawPointLight(graphics, new Vector3(0, 25, 0), Color.White, 30, 1, cameraPosition, view, projection);
            //DrawPointLight(graphics, new Vector3(0, 0, 70), Color.Wheat, 55 + 10 * (float)Math.Sin(5 * angle), 3, cameraPosition, view, projection);

            graphics.BlendState = BlendState.Opaque;
            graphics.DepthStencilState = DepthStencilState.None;
            graphics.RasterizerState = RasterizerState.CullCounterClockwise;

            graphics.SetRenderTarget(null);

            //Combine everything
            finalCombineEffect.Parameters["colorMap"].SetValue(colorRT);
            finalCombineEffect.Parameters["lightMap"].SetValue(lightRT);
            finalCombineEffect.Parameters["halfPixel"].SetValue(halfPixel);

            foreach (EffectPass pass in finalCombineEffect.CurrentTechnique.Passes)
            {
                pass.Apply();
                //finalCombineEffect.Techniques[0].Passes[0].Apply();
                quadRender.Draw(graphics, Vector2.One * -1, Vector2.One);
            }

            //Output FPS and 'credits'
            //double fps = (1000 / gameTime.ElapsedGameTime.TotalMilliseconds);
            //fps = Math.Round(fps, 0);
            //Game.Window.Title = "Deferred Rendering by Catalin Zima, converted to XNA4 by Roy Triesscheijn. Drawing " + (n * 4 + 3) + " lights at " + fps.ToString() + " FPS";
        }
        #endregion

        private void DrawScene(GraphicsDevice graphics, GameTime gameTime, Vector3 cameraPosition, Matrix view, Matrix projection)
        {
        //    graphics.DepthStencilState = DepthStencilState.Default;
        //    graphics.BlendState = BlendState.AlphaBlend;

            graphics.RasterizerState = RasterizerState.CullCounterClockwise;
            graphics.DepthStencilState = DepthStencilState.Default;
            //graphics.BlendState = BlendState.Additive;
            graphics.SamplerStates[0] = SamplerState.LinearWrap;

            foreach (ModelInstance instance in instances)
            {
                // Copy any parent transforms.
                Matrix[] transforms = new Matrix[models[instance.ModelId].Model.Bones.Count];
                models[instance.ModelId].Model.CopyAbsoluteBoneTransformsTo(transforms);

                // Draw the model. A model can have multiple meshes, so loop.
                foreach (ModelMesh mesh in models[instance.ModelId].Model.Meshes)
                {
                    // This is where the mesh orientation is set, as well 
                    // as our camera and projection.
                    foreach (Effect effect in mesh.Effects)
                    {
                        if (effect is BasicEffect)
                        {
                            (effect as BasicEffect).World = transforms[mesh.ParentBone.Index] *
                               Matrix.CreateScale(models[instance.ModelId].Scale) *
                                            Matrix.CreateScale(instance.Scale) *
                                            Matrix.CreateRotationX(models[instance.ModelId].Rotation.X) *
                                            Matrix.CreateRotationX(instance.Rotation.X) *
                                            Matrix.CreateRotationY(models[instance.ModelId].Rotation.Y) *
                                            Matrix.CreateRotationY(instance.Rotation.Y) *
                                            Matrix.CreateRotationZ(models[instance.ModelId].Rotation.Z) *
                                            Matrix.CreateRotationZ(instance.Rotation.Z) *

                                            Matrix.CreateTranslation(instance.Positon);
                            (effect as BasicEffect).View = view;
                            (effect as BasicEffect).Projection = projection;
                        }
                        else
                        {
                            effect.Parameters["World"].SetValue(
                                transforms[mesh.ParentBone.Index] *
                               Matrix.CreateScale(models[instance.ModelId].Scale) *
                                            Matrix.CreateScale(instance.Scale) *
                                            Matrix.CreateRotationX(models[instance.ModelId].Rotation.X) *
                                            Matrix.CreateRotationX(instance.Rotation.X) *
                                            Matrix.CreateRotationY(models[instance.ModelId].Rotation.Y) *
                                            Matrix.CreateRotationY(instance.Rotation.Y) *
                                            Matrix.CreateRotationZ(models[instance.ModelId].Rotation.Z) *
                                            Matrix.CreateRotationZ(instance.Rotation.Z) *

                                            Matrix.CreateTranslation(instance.Positon));
                            effect.Parameters["View"].SetValue(view);
                            effect.Parameters["Projection"].SetValue(projection);
                        }
                    }
                    // Draw the mesh, using the effects set above.
                    mesh.Draw();
                }
            }

            //foreach (Light l in lights.Where(l => l.Type == LightType.Point))
            //{
            //    foreach (ModelMesh mesh in sphereModel.Meshes)
            //    {
            //        foreach (BasicEffect effect in mesh.Effects)
            //        {
            //            effect.World = Matrix.CreateTranslation(l.Position);
            //            effect.View = view;
            //            effect.Projection = projection;

            //            mesh.Draw();
            //        }
            //    }
            //}
        }
    }
}
